/*jslint bitwise: true, newcap: true, eqeqeq: true, nomen: true, onevar: true,
        plusplus: true, regexp: true, undef: true, white: true */
// modified for use with WebLayout 2.0.1+
/**
 * Selector utility - implements CSS 2.1 selectors for easy traversing of DOM
 * Code quality has been valided by JSLint tool, see http://www.jslint.com for
 * more details. This code uses only good parts and function always return
 * a value; except for functions that always return undefined.
 * The only one defined global variable is $. To change the name, edit the row
 * number 435.
 *
 * @class Selector
 * @author Martin "DevelX" Jurča
 * @version 1.3
 */
"use strict";
(function () {
    var parsePath, filterByTagName, filterElements, select, getAllAncestors,
            checkArguments, filterByClass, filterById, filterByPreceded,
            filterByParent, filterByPseudoClass, filterByAttributes,
            filterElementAttributeValues, document;

    document = this.document;

    /**
     * Filteres through the DOM by given path starting at provided context.
     *
     * @param {String} path CSS 2.1 Selectors compatible path.
     * @param {HTMLElement} context Optional element to be used as root. If
     *        context is not provided, function uses body element.
     */
    select = function (path, context) {
        if (context === undefined) {
            context = document;
        }
        checkArguments([path, context], ['string', 'object']);
        var pathParts, elements, i, length;
        elements = [context];
        pathParts = parsePath(path);
        length = pathParts.length;
        for (i = 0; i < length; i += 1) {
            if (pathParts[i] !== '') {
                elements = filterElements(elements, pathParts[i]);
            }
        }
        return elements;
    };

    /**
     * Parses path into array of path parts.
     *
     * @private
     * @param {String} path Path to parse.
     * @return {Array} Path parts.
     */
    parsePath = function (path) {
        checkArguments(arguments, ['string']);
        path = path.replace(/[ ]+([+]|>)[ ]+/g, '$1').replace(/[ ]+/g, ' ');
        return path.split(' ');
    };

    /**
     * Filters ancestors of elements by given path.
     *
     * @private
     * @param {Array} elements HTML elements of which ancestors should be
     *        filtered.
     * @param {String} path Single path part from path parsed into array of path
     *        parts.
     * @return {Array} Filtered ancestors of given elements.
     */
    filterElements = function (elements, path) {
        checkArguments(arguments, [Array, 'string']);
        if (path === '*') {
            return getAllAncestors(elements);
        }
        if (path.indexOf('.') > -1) {
            return filterByClass(elements, path);
        }
        if (path.indexOf('#') > -1) {
            return filterById(elements, path);
        }
        if (path.indexOf('+') > -1) {
            return filterByPreceded(elements, path);
        }
        if (path.indexOf('>') > -1) {
            return filterByParent(elements, path);
        }
        if (path.indexOf(':') > -1) {
            return filterByPseudoClass(elements, path);
        }
        if (path.indexOf('[') > -1) {
            return filterByAttributes(elements, path);
        }
        return filterByTagName(elements, path);
    };

    /**
     * Filters elements by attributes and attribute values.
     *
     * @private
     * @param {Array} elements Array of elements of which ancestors should be
     *        filtered.
     * @param {String} path Path part.
     * @return {Array} Array of filtered ancestor elements.
     */
    filterByAttributes = function (elements, path) {
        checkArguments(arguments, [Array, 'string']);
        var result, parts = path.split('['), subresult, i, attValNotKnown,
                attValList, attHyphenList, attVal, length;
        subresult = filterElements(elements, parts[0]);
        length = parts.length;
        for (i = 1; i < length; i += 1, subresult = result) {
            result = [];
            parts[i] = parts[i].substring(0, parts[i].length - 1);
            attValNotKnown = parts[i].indexOf('=') === -1;
            attValList = parts[i].charAt(parts[i].indexOf('=') - 1) === '~';
            attHyphenList = parts[i].charAt(parts[i].indexOf('=') - 1) === '|';
            attVal = parts[i].replace(/^[^"]+["]([^"]*)["]$/, '$1');
            parts[i] = attValNotKnown ? parts[i] : parts[i].substring(0,
                    parts[i].indexOf('=') - (attValList || attHyphenList ?
                    1 : 0));
            filterElementAttributeValues(result, subresult, attValNotKnown,
                    attValList, attHyphenList, attVal, parts[i]);
        }
        return result;
    };

    /**
     * Aid function for filterByAttributes.
     *
     * @private
     * @param {Array} result Filtering result.
     * @param {Array} subresult Filtering subresult.
     * @param {boolean} attValNotKnown True when the attribute value doesn't
     *        matter.
     * @param {boolean} attValList True when attribute value is set of space
     *        separeted values.
     * @param {boolean} attHyphenList True when attribute value should start
     *        with attVal.
     * @param {String} attVal Desired value of attribute.
     * @param {String} part Attribute name.
     */
    filterElementAttributeValues = function (result, subresult, attValNotKnown,
            attValList, attHyphenList, attVal, part) {
        var j, k, vals, attribute, length, attributes;
        length = subresult.length;
        for (j = 0; j < length; j += 1) {
            attributes = subresult[j].attributes;
            if (attValNotKnown) {
                if (attributes[part] && attributes[part].specified) {
                    result.push(subresult[j]);
                }
            } else {
                attribute = attributes[part] && attributes[part].value;
                if (attValList && (typeof attribute === 'string')) {
                    vals = attribute.split(' ');
                    for (k = vals.length - 1; k >= 0; k -= 1) {
                        if (vals[k] === attVal) {
                            result.push(subresult[j]);
                            break;
                        }
                    }
                } else if (attHyphenList) {
                    if ((typeof attribute === 'string') &&
                            (attribute.substring(0, attVal.length) ===
                            attVal)) {
                        result.push(subresult[j]);
                    }
                } else {
                    if ((attributes[part] &&
                            attributes[part].value) === attVal) {
                        result.push(subresult[j]);
                    }
                }
            }
        }
    };

    /**
     * Filters elements ancestors by pseudo-class... only :first-child is
     * (logicaly) supported.
     *
     * @private
     * @param {Array} elements Array of elements of which ancestors should be
     *        filtered.
     * @param {String} path Path part.
     * @return {Array} Array of filtered ancestor elements.
     */
    filterByPseudoClass = function (elements, path) {
        checkArguments(arguments, [Array, 'string']);
        var result = [], parts = path.split(':', 2), parents, i, node, length;
        parents = filterElements(elements, parts[0]);
        length = parents.length;
        for (i = 0; i < length; i += 1) {
            if (parents[i].firstChild !== null) {
                node = parents[i].firstChild;
                while ((node !== null) && (node.nodeType !== 1)) {
                    node = node.nextSibling;
                }
                if (node !== null) {
                    result.push(node);
                }
            }
        }
        return result;
    };

    /**
     * Filters elements by their parents.
     *
     * @private
     * @param {Array} elements Array of elements of which ancestors should be
     *        filtered.
     * @param {String} path Path part expressing relation between result
     *        elements and their parents.
     * @return {Array} Array of filtered ancestors.
     */
    filterByParent = function (elements, path) {
        checkArguments(arguments, [Array, 'string']);
        var result = [], children, parts, i, parents, j, include, length;
        parts = path.split('>', 2);
        children = filterElements(elements, parts[1]);
        parents = filterElements(elements, parts[0]);
        length = children.length;
        for (i = 0; i < length; i += 1) {
            if (children[i].nodeType === 1) {
                include = false;
                for (j in parents) {
                    if (children[i].parentNode === parents[j]) {
                        include = true;
                    }
                }
                if (include) {
                    result.push(children[i]);
                }
            }
        }
        return result;
    };

    /**
     * Filters elements by their preceeders.
     *
     * @private
     * @param {Array} elements Array of elements of which ancestors should be
     *        filtered.
     * @param {String} details Description of relationship between ancestor
     *        elements.
     * @return {Array} Array of filtered ancestor elements.
     */
    filterByPreceded = function (elements, details) {
        checkArguments(arguments, [Array, 'string']);
        var result = [], parts, i, precessors, followers, include, j, node,
                length;
        parts = details.split('+', 2);
        precessors = filterElements(elements, parts[0]);
        followers = filterElements(elements, parts[1]);
        length = followers.length;
        for (i = 0; i < length; i += 1) {
            if (followers[i].nodeType === 1) {
                include = false;
                for (j = precessors.length - 1; j >= 0; j -= 1) {
                    if (precessors[j].nodeType === 1) {
                        node = precessors[j].nextSibling;
                        while ((node !== null) && (node !== followers[i])) {
                            node = node.nextSibling;
                        }
                        if (node !== null) {
                            include = true;
                        }
                    }
                }
                if (include) {
                    result.push(followers[i]);
                }
            }
        }
        return result;
    };

    /**
     * Filters ancestor elements by id.
     *
     * @private
     * @param {Array} elements Array of elements of which ancestors should be
     *        filtered.
     * @param {String} id Id of element(s) with (optional) tag name.
     * @return {Array} Array of filtered ancestor elements.
     */
    filterById = function (elements, id) {
        checkArguments(arguments, [Array, 'string']);
        var result = [], ancestors, i;
        id = id.split('#', 2);
        if (id[0] !== '') {
            ancestors = filterByTagName(elements, id[0]);
        } else {
            ancestors = getAllAncestors(elements);
        }
        for (i in ancestors) {
            if (ancestors[i].id === id[1]) {
                result.push(ancestors[i]);
            }
        }
        return result;
    };

    /**
     * Filters ancestor elements by class.
     *
     * @private
     * @param {Array} elements Array of elements of which ancestors should be
     *        filtered.
     * @param {String} className Class name with (optional) tag name.
     * @return {Array} Array of filtered elements.
     */
    filterByClass = function (elements, className) {
        checkArguments(arguments, [Array, 'string']);
        var result = [], ancestors = [], i, j, names, k, push, pushPartial,
                length;
        className = className.split('.');
        if (className[0] !== '') {
            ancestors = filterByTagName(elements, className[0]);
        } else {
            ancestors = getAllAncestors(elements);
        }
        length = ancestors.length;
        for (i = 0; i < length; i += 1) {
            if (typeof ancestors[i].className === 'string') {
                push = true;
                for (k = 1; k < className.length; k += 1) {
                    if (ancestors[i].className.indexOf(' ') > -1) {
                        names = ancestors[i].className.split(/\s+/);
                        pushPartial = false;
                        for (j = names.length; j >= 0; j -= 1) {
                            if (names[j] === className[k]) {
                                pushPartial = true;
                            }
                        }
                        push = pushPartial;
                    } else if (ancestors[i].className !== className[k]) {
                        push = false;
                    }
                }
                if (push) {
                    result.push(ancestors[i]);
                }
            }
        }
        return result;
    };

    /**
     * Returns all ancestors of provided elements.
     *
     * @private
     * @param {Array} elements Array of elements of which ancestors should be
     *        returned.
     * @return {Array} All ancestors of provided elements.
     */
    getAllAncestors = function (elements) {
        checkArguments(arguments, [Array]);
        var i, result, element, length;
        result = [];
        length = elements.length;
        for (i = 0; i < length; i += 1) {
            if (elements[i].firstChild !== undefined) {
                element = elements[i].firstChild;
                while (element !== null) {
                    if (element.nodeType === 1) {
                        result = result.concat(element,
                                getAllAncestors([element]));
                    }
                    element = element.nextSibling;
                }
            }
        }
        return result;
    };

    /**
     * Filters ancestor elements by tag name.
     *
     * @private
     * @param {Array} elements Array of HTML element of which ancestors should
     *        be filtered.
     * @param {String} tagName Tag name of children to return.
     * @return {Array} Array of filtered children of given elements.
     */
    filterByTagName = function (elements, tagName) {
        checkArguments(arguments, [Array, 'string']);
        var result, i, j, subresult, length, length2;
        result = [];
        length = elements.length;
        for (i = 0; i < length; i += 1) {
            if (elements[i].getElementsByTagName !== undefined) {
                subresult = elements[i].getElementsByTagName(tagName);
                length2 = subresult.length;
                for (j = 0; j < length2; j += 1) {
                    if ((subresult[j].nodeType !== undefined) &&
                            (subresult[j].nodeType === 1)) {
                        result.push(subresult[j]);
                    }
                }
            }
        }
        return result;
    };

    /**
     * Checks types of arguments passed by reference to arguments object.
     *
     * @private
     * @param {Object} args arguments object of function requesting argument
     *        type check.
     * @param {Array} types Types of arguments. Use string for validation using
     *        typeof operator and constructor function for validation using
     *        instanceof.
     * @throw TypeError If any argument is of wrong type.
     */
    checkArguments = function (args, types) {
        var length;
        if (typeof args !== 'object') {
            throw new TypeError("args has to be arguments object");
        }
        if (!(types instanceof Array)) {
            throw new TypeError("types has to be an array");
        }
        var i;
        if (args.length !== types.length) {
            throw new RangeError("Invalid number of arguments: " +
                    types.length + " expected, " + args.length + " given");
        }
        length = args.length;
        for (i = 0; i < length; i += 1) {
            if (typeof types[i] === 'string') {
                if (typeof args[i] !== types[i]) {
                    throw new TypeError("Argument " + i + " has wrong type: " +
                            types[i] + " expected, " + (typeof args[i]) +
                            " given");
                }
            }
            if (types[i] instanceof Function) {
                if (!(args[i] instanceof types[i])) {
                    throw new TypeError("Argument " + i + " has wrong type: " +
                            types[i] + " expected, " + (typeof args[i]) +
                            " given");
                }
            }
        }
    };

    this.document.querySelectorAll = select;
    this.document.querySelector = function (query) {
        return select(query)[0];
    };
}());